Skip to content

Conversation

@hickford
Copy link
Contributor

@hickford hickford commented Nov 3, 2023

Add properties ICredential.PasswordExpiry and ICredential.OAuthRefreshToken. These correspond to Git credential attributes password_expiry_utc and oauth_refresh_token, see https://git-scm.com/docs/git-credential#IOFMT. Previously these attributes were silently disarded.

Plumb these properties from input to host provider to credential store to output.

Credential store support for these attributes is optional, marked by new properties CredentialStore.CanStorePasswordExpiry and ICredentialStore.CanStoreOAuthRefreshToken. Implement support in CredentialCacheStore, SecretServiceCollection and WindowsCredentialManager.

Add method IHostProvider.ValidateCredentialAsync. The default implementation simply checks expiry. Other implementations might query a server.

Improve implementations of GenericHostProvider and GitLabHostProvider. Previously, GetCredentialAsync saved credentials as a side effect. This is no longer necessary. The workaround to store OAuth refresh tokens under a separate service is no longer necessary assuming CredentialStore.CanStoreOAuthRefreshToken. Querying GitLab to check token expiration is no longer necessary assuming CredentialStore.CanStorePasswordExpiry.

Fixes #1463
Fixes #268
Fixes #2059

Add properties ICredential.PasswordExpiryUTC and
ICredential.OAuthRefreshToken. These correspond to Git credential
attributes password_expiry_utc and oauth_refresh_token, see
https://git-scm.com/docs/git-credential#IOFMT. Previously these
attributes were silently disarded.

Plumb these properties from input to host provider to credential store
to output.

Credential store support for these attributes is optional, marked by
new properties ICredentialStore.CanStorePasswordExpiryUTC and
ICredentialStore.CanStoreOAuthRefreshToken. Implement support in
CredentialCacheStore, SecretServiceCollection and
WindowsCredentialManager.

Add method IHostProvider.ValidateCredentialAsync. The default
implementation simply checks expiry.

Improve implementations of GenericHostProvider and GitLabHostProvider.
Previously, GetCredentialAsync saved credentials as a side effect. This
is no longer necessary. The workaround to store OAuth refresh tokens
under a separate service is no longer necessary assuming
CredentialStore.CanStoreOAuthRefreshToken. Querying GitLab to check
token expiration is no longer necessary assuming
CredentialStore.CanStorePasswordExpiryUTC.
@hickford hickford force-pushed the expiry-and-oauth-refresh-token branch 3 times, most recently from a2425b9 to a21c066 Compare November 3, 2023 21:48
@hickford hickford changed the title introduce PasswordExpiryUTC and OAuthRefreshToken introduce PasswordExpiry and OAuthRefreshToken Nov 3, 2023
@hickford hickford force-pushed the expiry-and-oauth-refresh-token branch from a21c066 to 2a14b66 Compare November 4, 2023 06:36
@hickford hickford changed the title introduce PasswordExpiry and OAuthRefreshToken Store PasswordExpiry and OAuthRefreshToken Nov 10, 2023
@hickford hickford added the enhancement New feature or request label Nov 10, 2023
@hickford hickford marked this pull request as ready for review November 10, 2023 20:07
@hickford
Copy link
Contributor Author

Windows build ought to be fixed by #1418

@hickford hickford force-pushed the expiry-and-oauth-refresh-token branch 2 times, most recently from 83c3f3a to b59547b Compare February 22, 2024 20:14
Copy link
Contributor

@ldennington ldennington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good! Most of my comments are small things - the only major issue that I'd need to see updated before approving is not having expiry implemented for Windows Credential Manager.

LLNobles

This comment was marked as spam.

@hickford hickford force-pushed the expiry-and-oauth-refresh-token branch 4 times, most recently from 01fcd91 to fdd7759 Compare November 1, 2024 20:46
@hickford hickford force-pushed the expiry-and-oauth-refresh-token branch from fdd7759 to 60224e0 Compare December 6, 2024 19:35
Copy link
Contributor

@ldennington ldennington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the updates!

Marshal.FreeHGlobal and Marshal.DestroyStructure
@LLNobles

This comment has been minimized.

Limit iterations in GetCredentialAsync
@hickford hickford requested a review from PetSerAl January 3, 2025 07:56
Copy link

@PetSerAl PetSerAl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion (feal free to disregard it): it is better not to remove refresh token unnecessary. I understand, that GCM can just reauthenticate, but that require extra, otherwise unnecessary, browser interaction from user, thus providing slightly worse user experience.

Anyways, I have no intentions to spend my time to pursue this. Error rate unlikely would be that hi to bother me that much.

@hickford
Copy link
Contributor Author

@mjcheetham any time to look at this?

@LLNobles

This comment has been minimized.

@dscho
Copy link
Contributor

dscho commented Feb 26, 2025

@mjcheetham any time to look at this?

@hickford: @mjcheetham is no longer working at GitHub (but is once again my teammate, this time at Microsoft). Git Credential Manager, however, is still owned by GitHub and it is currently not clear to me who at GitHub takes care of this here repository.

@LLNobles

This comment has been minimized.


// Return the new access token
return new GitCredential(oauthUser,refreshResult.AccessToken);
return new GitCredential(refreshResult, oauthUser);
Copy link
Contributor

@becm becm Mar 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delaying the save of a refresh_token will likely result in issues.

New value must be saved, even if access_token is invalid for the requested operation.
This means we'd have to check (and rely on git) to also provide the new refresh_token and store its value during an erase callback (needs ugly hack in EraseCredentialAsync).

Granted the current state (without #1838) is also broken. (fix has been merged)
I think this is still the correct location to directly issue a store.
Otherwise there is a risk of client desync due to missed refresh_token value updates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are advantages and disadvantages to pre-emptively storing the refresh token. In particular, storing the wrong username if the user has multiple accounts on the host and selects the wrong account. In general, I'd prefer to avoid the complexity of side effects. The Git credential API has deliberate independent verbs "get/store/erase".

Which host do you test with?

Copy link
Contributor

@becm becm Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chances are high the 1st action is a pull from a repository readable by the 2nd account.
So any existing entry is likely to be replaced anyway (no failure as account guard). ☺

Git can only give feedback (store vs. erase) on the access_token.
Any provided refresh_token is (by convention) valid and MUST be used in further interactions.
Saving that during the get phase is not a side effect but required as part of a completed transaction.
Delaying the save can break things on Ctrl+C, parallel pulls, or stupid proxies
(see comments regarding missing token updates).

The primitive get with detached store/erase is broken in the context of OAuth.
A 2nd parallel action from the same host will trigger another token refresh and a conflicting store
(not an issue to have multiple access_token, but potentially fatal with an outdated refresh_token) .

My primary endpoint is Forgejo with recommended single-use tokens (→ #1838 required).
Primary client is Win11 (where leaking refresh_token in the output would be only a minor violation).
(→ getting to the next potential problem with the get/store approach on a proper secret store…)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay -- I've restored that logic. Would you be able to test this branch with Forgejo?

["password"] = testPassword
["password"] = testPassword,
["password_expiry_utc"] = testExpiry.ToString(),
["oauth_refresh_token"] = testRefreshToken,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refresh_token will likely also have an expiry date.
At least one observed implementation however does not report the constraint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestion to improve the test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, wrong/stupid location for my comment (test is not the issue). ☹
(Maybe you can mark this part of my comments as resolved to avoid further confusion?)

General problem with the auth flow when not dealing with refresh in the get phase:

  • presenting git with an expired credential will just lead to ignoring it
  • the refresh_token might also be expired (but git would have no way of recovering anyway…)

Telling git should only be useful if there was a further provider with valid credentials as a fallback…
But since GCM has no way of knowing this, it's likely best to keep handling refresh proactively in get.
Which would of course render the whole concept of supplying any token metadata to git redundant.

@hickford hickford requested review from a team as code owners November 11, 2025 21:10
@becm
Copy link
Contributor

becm commented Jan 11, 2026

Hm, lumping together these 2 distinct entities opens a can of worms when trying to do updates consistently.

  • update of refresh_token can not be missed in any code path
  • erase may NOT invalidate the refresh_token part (as hinted by @PetSerAl)

So this would mean (to be consistent with current/fallback behavior):

  • during initial registration, a partial secret must already be constructed and saved (invalid access_token)
  • during refresh a partial update of the secret is required (refresh_token only)
  • the erase must be limited to the access_token part
    (if refresh_token is the same value, else it came from a successful git call that was started later)

When updating the access_token part, the value for refresh_token MUST be read again,
it may have been updated in the time window between token refresh and token store by a parallel git call.

The whole approach of partial updates also massively increases potential for conflicting parallel operations.
Now: Each get needs consistent read/store
After: Each store/erase also has the potential to mess something up.
Not sure how often these conflicts appear in regular use, but the devil is in the details…
And each failure will lead to client invalidation in case of single-use refresh_token requirements.

@becm
Copy link
Contributor

becm commented Jan 11, 2026

To be honest, in the OAuth case I would opt to

  • store the access_token during the get phase and
  • make store and erase a NOOP.

This is likely to reduce potential conflicts even in the current approach.
But there may be reservations to deviate even more from the classical git credential flow.
(Which in my opinion only fits well with a fully interactive flow, but not matching well with OAuth exchanges)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Credential protocol enhancement: OAuth token expiration and refresh tokens Store oauth_refresh_token Add basic validation of stored credentials

7 participants